|
Inside OP VCL / Windows Messages / Delphi 1-5
Modifying VCL Behavior A Practical Example Using Visual Components
To make a visual component behave differently from its defaults, we generally have to create a new component that descends from the original component's class. This article will show how to dynamically change the behavior of a native Delphi visual component without creating a new class.
How is this possible? The secret is to intercept the Windows messages being sent to the control. This can be accomplished by using a TControl property named WindowProc, which essentially points to a component's Windows message event handler.
To demonstrate this technique, we'll create a LinkedLabel component, which will link itself to any TControl and dynamically modify its behavior. TLinkedLabel will descend from TLabel, and will feature four additional published properties:
In addition, TLinkedLabel will keep the Enabled and Visible properties of the LinkedLabel and its associate synchronized. It will also maintain a set distance and orientation from the associate control. This means that when you move the LinkedLabel, the associate moves with it, and vice versa.
Let's take a look at the TLinkedLabel class declaration, shown in Figure 1.
unit LinkedLabel;
interface
uses Messages, Classes, Controls, StdCtrls;
type TLinkedLabel = class(TLabel) private // The associate control. FAssociate: TControl; // Puts FAssociate into all caps mode. FCapsLock: Boolean; // The distance between the label and the associate. FGap: Integer; // True when the label is on top of the associate. FOnTop: Boolean; // Saves the original value of FAssociate.WindowProc. FOldWinProc: TWndMethod; // Used to prevent infinite update loops. FUpdating: Boolean; protected procedure Adjust(MoveLabel: Boolean); procedure SetGap(Value: Integer); procedure SetOnTop(Value: Boolean); procedure SetAssociate(Value: TControl); procedure NewWinProc(var Message: TMessage); procedure Notification(AComponent: TComponent; Operation: TOperation); override; procedure WndProc(var Message: TMessage); override; public constructor Create(AOwner :TComponent); override; destructor Destroy; override; published property Associate: TControl read FAssociate write SetAssociate; property CapsLock: Boolean read FCapsLock write FCapsLock; property Gap: Integer read FGap write SetGap default 8; property OnTop: Boolean read FOnTop write SetOnTop; end; Figure 1: The TLinkedLabel class declaration.
Now let's look at the different methods of this component in detail, starting with the constructor. Note that when creating a new object, all of its associated memory is cleared. This will automatically set FAssociate and FOldWinProc to nil, and FCapsLock, FOnTop, and FUpdating to False, all without having to explicitly initialize them in the constructor. Therefore, the only thing we need to set in the constructor is the default Gap value:
implementation
constructor TLinkedLabel.Create(AOwner: TComponent); begin inherited; FGap := 8; end;
Now we come to the Adjust method, which is responsible for positioning the LinkedLabel component or the associate control, depending on the value of the MoveLabel parameter. As you'll see in the code, the actual position of the LinkedLabel in relationship to the associate is based on the Gap and OnTop properties (see Figure 2). Although OnTop only provides us with two possible orientations, there are many other possibilities that could easily be programmed into this component. However, adding a lot of "bells and whistles" to TLinkedLabel is not the focus of this article, and has, therefore, been entrusted to the reader.
procedure TLinkedLabel.Adjust(MoveLabel: Boolean); var dx, dy: Integer; begin if (Assigned(FAssociate)) then begin if (FOnTop) then begin dx := 0; dy := Height + FGap; end else begin dx := Width + FGap; dy := (Height - FAssociate.Height) div 2; end; if (MoveLabel) then begin Left := FAssociate.Left - dx; Top := FAssociate.Top - dy; end else begin FAssociate.Left := Left + dx; FAssociate.Top := Top + dy; end; end; end; Figure 2: The Adjust method.
At this point, we come to the set methods of the Gap and OnTop properties (see Figure 3). These are needed so we can reposition the LinkedLabel when the Gap or OnTop values are modified.
procedure TLinkedLabel.SetGap(Value: Integer); begin if (FGap <> Value) then begin FGap := Value; Adjust(True); end; end;
procedure TLinkedLabel.SetOnTop(Value: Boolean); begin if (FOnTop <> Value) then begin FOnTop := Value; Adjust(True); end; end; Figure 3: The set methods of the Gap and OnTop properties.
Now we come to the SetAssociate method (see Figure 4).
procedure TLinkedLabel.SetAssociate(Value: TControl); begin if (Value <> FAssociate) then begin if (Assigned(FAssociate)) then FAssociate.WindowProc := FOldWinProc; FAssociate := Value; if (Assigned(Value)) then begin Adjust(True); Enabled := FAssociate.Enabled; Visible := FAssociate.Visible; FOldWinProc := FAssociate.WindowProc; FAssociate.WindowProc := NewWinProc; end; end; end; Figure 4: The SetAssociate method.
To understand it, we need to discuss the WindowProc property in more detail. WindowProc is defined as of type TWndMethod. TWndMethod can be found in the Controls unit with the following definition:
TWndMethod = procedure(var Message: TMessage) of object;
Notice that FOldWinProc is also defined as a TWndMethod, and that the NewWinProc method has the same parameter structure as TWndMethod. This allows us to point FOldWinProc to the current value of WindowProc, and assign WindowProc to the NewWinProc method.
Why do we need to use FOldWinProc if WindowProc is just another event property? Because the difference between WindowProc and any other event property is that WindowProc is already pointing to an existing event handler. If we simply point WindowProc to our own method, the control will no longer be able to respond to any Windows messages. To solve this problem, we set FOldWinProc to the current value of WindowProc, before pointing WindowProc to the NewWinProc method.
In NewWinProc, we call the old message handler, via FOldWinProc, after and acting upon specific Windows messages. Because we modify the WindowProc property on the associate control, it's important that we restore its former value before changing to a new associate component.
It's also important that we don't leave the associate's WindowProc property pointing to a routine that no longer exists. We therefore call SetAssociate(nil) in the destructor, which, as we've seen, will restore WindowProc to its original value:
destructor TLinkedLabel.Destroy; begin SetAssociate(nil); inherited; end;
In addition, we don't want to be pointing to an associate that no longer exists. By overriding the Notification method, we can know when the associate control is destroyed, and reset our pointer to the associate accordingly:
procedure TLinkedLabel.Notification(AComponent: TComponent; Operation: TOperation); begin if ((Operation = opRemove) and (AComponent = FAssociate)) then SetAssociate(nil); end;
Now we come to the NewWinProc method (see Figure 5). Here, we simply look for specific Windows messages being sent to the associate component. It's important to realize that although this method is only called by the associate control, it's actually part of the LinkedLabel, i.e. Self = LinkedLabel, not the associate control. This is identical to creating an OnClick event handler for a button. The OnClick event handler is created as part of the button's parent form, and is not a new method extending the TButton class.
procedure TLinkedLabel.NewWinProc(var Message: TMessage); var Ch: Char; begin if (Assigned(FAssociate) and (not FUpdating)) then begin FUpdating := True; try case(Message.Msg) of WM_CHAR: if (FCapsLock) then begin Ch := Char(TWMKey(Message).CharCode); if (Ch >= 'a') and (Ch <= 'z') then TWMKey(Message).CharCode := ord(UpCase(Ch)); end; CM_ENABLEDCHANGED: Enabled := FAssociate.Enabled; CM_VISIBLECHANGED: Visible := FAssociate.Visible; WM_SIZE, WM_MOVE, WM_WINDOWPOSCHANGED: Adjust(True); end; finally FUpdating := False; end; end; FOldWinProc(Message); end; Figure 5: The NewWinProc method.
If you examine this routine, you'll see we make no attempt to process Windows messages. We react to specific messages, then let the associate process them normally by calling FOldWinProc. In the case of the WM_CHAR message, we change part of the message, causing the component to think an upper-case character was pressed.
Finally, we look at two different messages to see if the associate has been moved. This is because components that descend from TWinControl will get a WM_MOVE message when they're moved, while other visual components (such as a Label) will get the WM_WINDOWPOSCHANGED message. The WM_SIZE message is examined, because if the OnTop property is False, the position of the LinkedLabel will change based on the height of the component.
The last method of our component is where we make adjustments to the associate when the LinkedLabel is changed (see Figure 6). Rather than override existing methods of TLabel to do this, we employ the same technique we used to modify the associate's behavior. Notice that instead of tapping into the WindowProc property, we override the WndProc method. How is this the same technique? If you look at TControl's constructor, you'll see that WindowProc is initialized to point at the WndProc method. So in essence, we are overriding the same method, but in a cleaner way, and without having to store the original value of WindowProc.
procedure TLinkedLabel.WndProc(var Message: TMessage); begin if (Assigned(FAssociate) and (not FUpdating)) then begin FUpdating := True; try case(Message.Msg) of CM_ENABLEDCHANGED: FAssociate.Enabled := Enabled; CM_VISIBLECHANGED: FAssociate.Visible := Visible; WM_WINDOWPOSCHANGED: Adjust(False); end; finally FUpdating := False; end; end; inherited; end; Figure 6: Instead of tapping into the WindowProc property, we override the WndProc method.
One final point should be made about the previous component. You'll notice the use of FUpdating in both NewWinProc and WndProc. This variable is used to alert the LinkedLabel and the associate that the other component is making a change. If you don't do this, it's easy to create an infinite updating loop, or get unexpected results. Here's one flow of events that demonstrates the need for the FUpdating variable:
As you can see, we haven't even gotten a chance to change the associate's Top property to match the LinkedLabel's new position before the associate tries to move the LinkedLabel. By using the FUpdating variable, the associate will not notice the WM_MOVE message and won't try to call Adjust to reposition the LinkedLabel.
A Couple of Issues There are a couple of problems with the TLinkedLabel component that I did not address in this article. The following are brief descriptions:
Conclusion That's about it. Replacing the WindowProc of an existing component does have its limitations, but can be a very useful technique. I can't think of any other reasonable way to design a component like TLinkedLabel and have the associate control move the LinkedLabel when the associate is moved. I'm not going to try and list other possible uses for this technique, because they are countless and limited only by a programmer's ingenuity.
All source referenced in this article is available for download.
Jeremy Merrill is an EDS contractor in a partnership contract with the Veteran's Health Administration. He is a member of the VA's Computerized Patient Record System development team, located in the Salt Lake City Chief Information Officer's Field Office.
|
|